﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Reflection;
using System.Collections;
using System.ComponentModel;

namespace Moq
{
    public static class RandomExtensions
    {
        public static bool NextBool(this Random rand)
        {
            for (int i = 0; i < rand.Next(0, 10); i++)
            {
                rand.Next(1, 10);
            }
            if (rand.Next(1, 3) == 1) return false;
            else return true;
        }
        public static Decimal NextDecimal(this Random rand)
        {
            return Convert.ToDecimal(rand.NextDouble());
        }
        public static DateTime NextDateTime(
            this Random rand
            , DateTime min
            , DateTime max
            )
        {
            return DateTime.FromOADate((max.ToOADate() - min.ToOADate()) * rand.NextDouble() + min.ToOADate());
        }
        public static DateTime NextDateTime(
            this Random rand
            )
        {
            return rand.NextDateTime(DateTime.MinValue, DateTime.MaxValue);
        }

        static char[] charList = GetCapitalLetters().Union(
            GetCursiveLetters().Union(
            GetNumbers()))
            .ToArray();

        public static string NextString(this Random rand, int minLength = 1, int maxLength = 10)
        {
            var length = (minLength != maxLength) ? rand.Next(minLength, maxLength + 1) : minLength;
            var sb = new StringBuilder();
            for (int i = 0; i < length; i++)
                sb.Append((char)rand.NextFrom(charList));
            return sb.ToString();
        }

        public static IEnumerable<char> GetCapitalLetters()
        {
            for (char i = 'A'; i <= 'Z'; i++)
                yield return i;
        }
        public static IEnumerable<char> GetCursiveLetters()
        {
            for (char i = 'a'; i <= 'z'; i++)
                yield return i;
        }
        public static IEnumerable<char> GetNumbers()
        {
            for (char i = '0'; i <= '9'; i++)
                yield return i;
        }


        public static T NextFrom<T>(this Random rand, IEnumerable<T> items)
        {
            var lstItems = items.ToArray();
            var i = rand.Next(0, lstItems.Length);
            return lstItems[i];
        }
        public static T NextFrom<T>(this Random rand, params T[] items)
        {
            return rand.NextFrom((IEnumerable<T>)items);
        }

        public static object Next(this Random rnd, Type objectType)
        {
            var tc = Type.GetTypeCode(objectType);
            switch (tc)
            {
                case TypeCode.Boolean:
                    return (Convert.ChangeType(rnd.NextBool(), tc));
                    break;
                case TypeCode.Byte:
                    var b = new byte[1] { 0 };
                    rnd.NextBytes(b);
                    return (Convert.ChangeType(b[0], tc));
                    break;
                case TypeCode.Char:
                    var s = rnd.NextString(1, 1);
                    return (Convert.ChangeType(s[0], tc));
                    break;
                case TypeCode.DBNull:
                    break;
                case TypeCode.DateTime:
                    return (Convert.ChangeType(rnd.NextDateTime(), tc));
                    break;
                case TypeCode.Decimal:
                    return (Convert.ChangeType(rnd.NextDecimal(), tc)); ;
                    break;
                case TypeCode.Double:
                    return (Convert.ChangeType(rnd.NextDouble(), tc));
                    break;
                case TypeCode.Empty:
                    break;
                case TypeCode.Int16:
                    return (Convert.ChangeType(rnd.Next(), tc));
                    break;
                case TypeCode.Int32:
                    return (Convert.ChangeType(rnd.Next(), tc));
                    break;
                case TypeCode.Int64:
                    return (Convert.ChangeType(rnd.Next(), tc));
                    break;
                case TypeCode.Object:
                    break;
                case TypeCode.SByte:
                    var sb = new byte[1] { 0 };
                    rnd.NextBytes(sb);
                    return (Convert.ChangeType(sb[0], tc));
                    break;
                case TypeCode.Single:
                    return (Convert.ChangeType(rnd.NextDouble(), tc));
                    break;
                case TypeCode.String:
                    return (Convert.ChangeType(rnd.NextString(), tc));
                    break;
                case TypeCode.UInt16:
                    return (Convert.ChangeType(rnd.Next(), tc));
                    break;
                case TypeCode.UInt32:
                    return (Convert.ChangeType(rnd.Next(), tc));
                    break;
                case TypeCode.UInt64:
                    return (Convert.ChangeType(rnd.Next(), tc));
                    break;
                default:
                    break;
            }

            throw new NotImplementedException();

        }

        public static T Next<T>(this Random rnd)
        {
            return (T)rnd.Next(typeof(T));
        }

    }

    public static class MockExtensions
    {
        static Random rnd = new Random();
        public static object NextMock(this Random rnd, Type objectType, TryGenerateRandomValueForPropertyDelegate customValueGenerator = null)
        {
            Moq.Mock res = null; //Uri s;
            var resType = typeof(Moq.Mock<>).MakeGenericType(objectType);
            res = (Moq.Mock)Activator.CreateInstance(resType);
            res.DefaultValue = Moq.DefaultValue.Mock;

            foreach (var item in objectType.GetProperties())
            {
                SetupMockPropertyForRandomReturn(res, item, customValueGenerator);
            }
            return res.Object;
        }
        public static T NextMock<T>(this Random rnd, TryGenerateRandomValueForPropertyDelegate customValueGenerator = null) where T : class
        {
            return (T)rnd.NextMock(typeof(T), customValueGenerator);
        }

        public delegate bool TryGenerateRandomValueForPropertyDelegate(PropertyInfo propInfo, out object randomValue);

        static MethodInfo mockSetReturnsDefaultMethodInfo = typeof(Moq.Mock).GetMethod("SetReturnsDefault");
        //static MethodInfo mockSetupGetMethodInfo = typeof(Moq.Mock<>).GetMethod("SetupGet");
        private static void SetupMockPropertyForRandomReturn(
            Moq.Mock mock
            , System.Reflection.PropertyInfo propInfo, TryGenerateRandomValueForPropertyDelegate generateRandomValue = null)
        {
            var mockObjectType = mock.GetType().GetGenericArguments()[0];
            var propType = propInfo.PropertyType;
            object defaultValue = null;
            if (generateRandomValue == null || (!generateRandomValue(propInfo, out defaultValue)))
                if (propInfo.IsDefined(typeof(DefaultValueAttribute), true))
                {
                    var dva = (DefaultValueAttribute)propInfo.GetCustomAttributes(typeof(DefaultValueAttribute), true).First();
                    defaultValue = dva.Value;
                }
                else defaultValue = rnd.NextMockOrObject(propType);
            if (propInfo.CanWrite && mockObjectType.IsClass)
            {
                propInfo.SetValue(mock.Object, defaultValue, new object[] { });
            }
            else
            {

                ///RANDOM VALUE per property TYPE implementation
                //var setReturnsDefault = mockSetReturnsDefaultMethodInfo.MakeGenericMethod(propType);
                //setReturnsDefault.Invoke(mock, new[] { defaultValue });


                ///RANDOM VALUE per each property implementation
                //Moq.Mock<TMock> s;s.SetupGet(mock=>mock.MyProperty1).Returns(propValue)
                var mockSetupGetMethodInfo = mock.GetType().GetMethod("SetupGet");
                var setupGet = mockSetupGetMethodInfo.MakeGenericMethod(propType);
                Expression getPropertyExpression = GetPropertyExpression(mockObjectType, propInfo);
                var isetup = setupGet.Invoke(mock, new[] { getPropertyExpression });
                //isetup = IReturnsGetter<TMock, TProperty>
                var returnsGetterType = typeof(Moq.Language.IReturnsGetter<,>)
                    .MakeGenericType(mockObjectType, propType);

                var returnsMethodInfo = returnsGetterType.GetMethod("Returns", new Type[] { propType });
                returnsMethodInfo.Invoke(isetup, new[] { defaultValue });
            }
        }

        /// <summary>
        /// returns Expression{Func{TMock,TResult}}
        /// m => m.MyProperty1
        /// </summary>
        /// <param name="propInfo"></param>
        /// <returns></returns>
        private static Expression GetPropertyExpression(Type objectType, PropertyInfo propInfo)
        {
            var delegateType = typeof(Func<,>).MakeGenericType(objectType, propInfo.PropertyType);
            var parameter = Expression.Parameter(objectType, "mockedObject");
            var res = Expression.Lambda(delegateType, Expression.Property(parameter, propInfo), parameter);
            return res;
        }

        public static object NextMockOrObject(this Random rnd, Type propType, TryGenerateRandomValueForPropertyDelegate customValueGenerator = null)
        {
            var tc = Type.GetTypeCode(propType);
            object defaultValue = null;
            switch (tc)
            {
                case TypeCode.Empty:
                    throw new InvalidOperationException("Type is empty");
                    break;
                case TypeCode.DBNull:
                    throw new InvalidOperationException("Type is empty");
                    break;
                case TypeCode.Object:
                    object res = null;
                    if (!TryGenerateIEnumerable(propType, out defaultValue, customValueGenerator))
                        if (!TryGenerateNullableStandardObjects(propType, out defaultValue))
                            if (!TryGenerateStandardObjects(propType, out defaultValue))
                                TryGenerateMock(propType, out defaultValue, customValueGenerator);
                    break;
                default:
                    defaultValue = rnd.Next(propType);
                    break;
            }
            return defaultValue;
        }

        private static bool TryGenerateNullableStandardObjects(Type propType, out object defaultValue)
        {
            defaultValue = null;
            if (!propType.IsGenericType) return false;

            //var nullableType = ;
            if (propType.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                var objType = propType.GetGenericArguments()[0];
                try
                {
                    defaultValue = rnd.NextMockOrObject(objType);
                    defaultValue = Activator.CreateInstance(propType, defaultValue);
                }
                catch
                {
                    return false;
                }
                return true;
            }
            return false;
        }

        private static bool TryGenerateStandardObjects(Type propType, out object defaultValue)
        {
            defaultValue = null;
            if (propType == typeof(System.Uri))
            {
                defaultValue = new Uri("/" + rnd.NextString(), UriKind.Relative);
                return true;
            }
            return false;
        }

        private static bool TryGenerateMock(Type propType, out object defaultValue, TryGenerateRandomValueForPropertyDelegate customValueGenerator = null)
        {
            defaultValue = rnd.NextMock(propType, customValueGenerator);
            return true;
        }

        private static bool TryGenerateIEnumerable(Type propType, out object defaultValue, TryGenerateRandomValueForPropertyDelegate customValueGenerator = null)
        {
            defaultValue = null;
            if (!propType.IsGenericType) return false;
            var genericType = propType.GetGenericTypeDefinition();
            var genericItemType = propType.GetGenericArguments()[0];

            var genericListType = typeof(List<>).MakeGenericType(genericItemType);
            IList genericList = (IList)Activator.CreateInstance(genericListType);
            var randLength = rnd.Next(0, 6);
            for (int i = 0; i < randLength; i++)
            {
                var item = rnd.NextMockOrObject(genericItemType, customValueGenerator
                    );
                genericList.Add(item);
            }

            if (propType.IsAssignableFrom(genericListType))
            {
                defaultValue = genericList;
                return true;
            }
            else if (propType.IsAssignableFrom(genericItemType.MakeArrayType()))
            {
                var toArrayMethod = genericListType.GetMethod("ToArray");
                defaultValue = toArrayMethod.Invoke(genericList, new object[] { });
                return true;
            }

            return false;
        }


    }

}
